6. Class
6.1 Basic definitions
A class is a structured collection of data. Related variables can be include in a class. To define a class, use the below syntax:
class {class_name}
...
end [class]
The body of a class ("..." in above syntax) can be declaration of member variables and member functions. To define a member variable, use the below syntax within the "class ... end":
public {variable_name} as {type}
or
private {variable_name} as {type}
Note: current version of Birdee does not allow assign initial values to member variables.
The "public" and "private" keywords specify the access modifier of the variable. We will discusses it a bit later. Now we write our first class:
class bird
public name as string
public weight as float
end
The class describes a bird with a name and a weight. You cannot use the member variables until you create an instance of the class. To create an instance of "bird" class, use "new" keyword:
dim mybird as bird = new bird
mybird.name = "Birdee"
mybird.wright = 2.3
println(mybird.name)
The member variables can be accessed by a "." after a class instance expression, with the name of the member variable. The above example creates an instance of class "bird" and assign it to a variable "mybird" (Note that the class name can be used as a type!). The example then assign the member variables of "mybird". The member variables belongs to the instances of the class. Thus, you cannot use them without a reference to an instance.
6.2 Explanations of terminologies
6.2.1 Class v.s. Class Instance v.s. Object
As defined above, a class is the definition of structured data. A class (for example, "bird" class) is a type for some data.
Class instance is a piece of concrete data of some class. "string" is a class, and a string variable is a class instance of string. Class instances are sometimes called "Objects".
Note that different instances of a class are independent with each other. Assigning the member variable of one instance will not affect the same member variable of others.
6.2.2 Member variables & fields
They have the same meaning.
6.2.3 Member functions & methods
They have the same meaning.
6.2.4 Reference
Once an object is allocated in memory, how can you find it and operate on it? The answer is by "reference". A reference points to a instance of class (or an array). Variables of class/array types holds the references to the instances in the memory (e.g. dim mybird as bird). So copying variables of class/array types copies the references to the instances, instead of copying the object/array themselves.
Here we introduce a special reference constant "null". It points to actually nothing. You can assign null to any reference typed variable (class, array). If a reference is null, it means that it is an empty value. You should never call a member method or get the field of a null reference, or an error will occur. You can check if a reference is null by:
if some_reference !== null then
...
end
Note that for class object variables, the initial values are null.
6.2.5 "this"
"this" is a reference to the current object in the member function. It is a Birdee keyword that can only be used in member functions. For Birdee code
obj.funct()
when it is run, in the method "funct", "this" will point to the object being called - in this case, "obj".
6.3 Member functions
Member functions can be defined within the scope of the class:
{access_modifier} function {function_name} ([ {parameter1} as {type}, {parameter2} as {type}, ... ]) [as {type}]
...
end [function]
It is similar to the definition of normal functions, except that member functions are defined inside a class, and there is an "access_modifier" at the beginning of the function. The "access_modifier" can be "public", "private" or omitted (which will be explained later).
We can write a member function in the above "bird" class:
class bird
public name as string
public weight as float
public function fly()
println("my name is "+ this.name + ". I am flying!")
end
end
You can call "fly" on an instance of "bird" class:
dim mybird as bird = new bird
mybird.name = "Birdee"
mybird.wright = 2.3
mybird.fly()
The above example of "bird" class uses a keyword "this" of Birdee to represent the current instance calling the member function (see the line: println("my name is "+ this.name + ". I am flying!") ) . When the member function is called ( mybird.fly() ), "this" will be a reference to the instance pointed by "mybird". "this" keyword cannot be used outside the class definition. Also, in the member functions, to use the member variables, "this" keyword can be omitted. For example "this.name" can be simplified to "name", when name is a member variable of the same class. Also note that for a member function, if there is a local variable (variables defined in the body of a function or in the arguments) having the same name of a member variable, using the name will result in using the local variable. To use the member variable have a conflicting name, use "this.XXX" instead. (XXX is the conflicting name).
Note that the member functions cannot be accessed without an instance.
6.4 Access modifier
We finally explain what is "private" and "public" in front of the member variables and functions. If a member variable or function is defined "private", no one can access it unless it is a member function of the same class, for example:
class bird
public name as string
public weight as float
private secret_name as string
end
dim mybird as bird = new bird
mybird.secret_name="sadas"
The above code will not compile, because "secret_name" is a private member. Access control (making members private/public) is useful when you have some internal variables or functions that you do not want other people to have access to. But remind that a member function can always access all members of the same class, regardless of being private or public.
Note that a member function's access modifier can be omitted when defining it, and the access is set to "public" by default.
6.5 Initialization & destruction
We can now create instances by "new". But what if we want to initialze the object while creating it? The syntax of "New with initialization" can be use.
new {class_name}.{method_name}([arg1,arg2,....])
The class_name is the name of the class of the instance to be created. The method_name is the member function name to be called after the creation of the object. The function should be a public function. Here is an example:
class bird
public name as string
public weight as float
public void init(name as string, weight as float)
this.name=name
this.weight=weight
end
end
dim mybird as bird = new bird.init("birdee",2.13)
The above example creates and initializes a "bird" object, which is equivalent to:
dim mybird as bird = new bird
mybird.init("birdee",2.13)
Note: the "New with initialization" will discard the return value of the called function and will always return the created instance.
When the class has a "__init__" method defined in the class body, users can use "New with initialization" in a simpler way. The following code:
dim obj = new SomeClass("hi")
will create an new instance of class SomeClass and call obj.__init__("hi") for initialization.
If the "__init__" method has no arguments, users can further save the "(...)" for calling "__init__":
class SomeClass
public function __init__()
end
end
dim obj = new SomeClass #__init__ will be called here.
Note that "__init__" must be a public function in the class.
Birdee uses garbage collection for memory management, which means when the object created is no longer used (when there are no references pointing to the object), the object will be automatically delected. If the object has "__del__" method defined, when it is deleted and, the method will be called. You can do some finalization work in this method.
6.6 Operator overloading
Operator overloading means you can apply basic operators (like +,-,*,/,...) on you own class and define your classes' own behavior on them.
We are acutally familar with the use of operator overload, which is used in "+" operator of string. (Don't forget that "string" is simply a class defined by Birdee!). We can use "+" to concatenate two strings:
println("string A" + "string B")
There is no magic in it and Birdee implements it using operator overload.
To overload operator "+", you must define a special member function "__add__" in your class:
class complex
public real as double
public imaginary as double
public function __add__(complex other) as complex
return new complex:set(this.real + other.real, this.imaginary + other.imaginary)
end
public function set(real as double,imaginary as double)
this.real=real
this.imaginary=imaginary
end
end
dim v1 = new complex:set(1,3), v2 = new complex:set(2,4)
dim v3 = v1 + v2
println(double2str(v3.real))
The above example defines a complex number class. It overloads "+" by the function "__add__". The function accepts another "complex" object and creates a new complex object as the result. It then creates two complex numbers (1,3) and (2,4), add them and store in variable "v3" and print the real part of the result. The line "dim v3 = v1 + v2" will be equivalent to:
dim v3 = v1.__add__(v2)
There are other operators that can be overloaded. The operators and functions to implement are listed in the below table:
Operator | Function Name to implement | Operand |
---|---|---|
+ | __add__ | The other object |
- | __sub__ | The other object |
* | __mul__ | The other object |
/ | __div__ | The other object |
% | __mod__ | The other object |
== | __eq__ | The other object |
!= | __ne__ | The other object |
>= | __ge__ | The other object |
<= | __le__ | The other object |
> | __gt__ | The other object |
< | __lt__ | The other object |
|| | __logic_or__ | The other object |
| | __or__ | The other object |
&& | __logic_and__ | The other object |
& | __and__ | The other object |
^ | __xor__ | The other object |
! | __not__ | None |
* (added before an expression) | __deref__ | None |
Array read | __getitem__ | The "index" |
Array write | __setitem__ | The "index" and the object to "put" in |
For a class object, if an operator is applied, the corresponding method will be called. If the method is not defined or it is private, a compile error will be raised.
For overloaded binary opeartors (operators with two opearnds), "A ? B", where "?" is any binary operators, is equivalent to "A.__XXXX__(B)", where "__XXXX__" is the corresponding method. For overloaded unary operators (operators with one operand), using them is equivalent to "A.__XXXX__()", where "__XXXX__" is the corresponding method.
Operators for array read & write are special cases for operator overloading. These two operators overloads the "[]" operator which is originally used for accessing array elements. If the indexed object is to be read "from" an object, the "__getitem__" method will be called. For Birdee code like
dim a = obj["123"]
where the variable "obj" is not an array, the compiler will first the "__getitem__" method of the class of "obj". The actual generated code will be
dim a = obj.__getitem__("123")
Similarly, if the indexed element is written (on the left of the "="), the method "__setitem__" will be called. The first parameter should be the index and the second parameter should be the value to be written to the object. For example, the following two lines of code have the same effect:
obj["123"]=34
obj.__setitem__("123",34)
The existence of the method "__setitem__" is optional, as long as you never "write" to an indexed element. However, if you want to overload "[]", the method "__getitem__" should always be defined in the class.
Note that the type of parameters of methods for operator overloading is not necessarily the same class of the current class. They can be any valid types.
Also note that you can use function templates for operator overloading. Templates will be later introduced.
6.7 Class inherit
6.7.1 Basic inherit
Class can inherit another class's public members through class inherit. The inherited class is also called parent class. A class with parent class is usually viewed as a specification of its parent.
The following codes:
class ParentClass
private a as int
public b as int
public function get() as int
return a
end
end
class SomeClass : ParentClass
public function __init__()
end
public function get2() as int
return b + get()
end
end
define a class named "SomeClass" with a parent class named "ParentClass", and SomeClass inherits member field "b" and member function "get()" from ParentClass, note that member field "a" is not inherited since it's private to ParentClass. Also, SomeClass can access the member it inherits inside class directly, as showed in the example.
Besides, the following code:
class ParentClass
public function __init__()
end
public function __del__()
end
public function __not__() as boolean
return true
end
end
class SomeClass : ParentClass
public function __init__()
end
public function __del__()
end
end
dim foo = new SomeClass
dim bar = !foo
shows an example of class inherit with special member functions. Note that in code "dim foo = new SomeClass", the function init() of SomeClass will be automatically called, but init() of ParentClass will not! Also, the del() of SomeClass will be automatically called when garbadge collected, but del() of ParentClass will not. And the code "dim bar = !foo" won't compile because SomeClass does not contain an operator overloading function for !. Even if SomeClass inherits one from ParentClass, the compiler will not automatically call it.
6.7.2 "super"
What if we want the functions in parent be called during above scenarios? We can use the "super" keyword. Similar to "this" keyword, "super" keyword represents a built-in reference inside class. However, "this" refers to the instance itself, while "super" refers to the parent part of the instance. That is to say, we can use "super" keyword to only access the members inherited from parent.
With "super" keyword, the above code can be modified to:
class ParentClass
public function __init__()
end
public function __del__()
end
public function __not__() as boolean
return true
end
end
class SomeClass : ParentClass
public function __init__()
super.__init__()
end
public function __del__()
super.__del__()
end
public function __not__() as boolean
return super.__not__()
end
end
dim foo = new SomeClass
dim bar = !foo
Then, the code will compile and the special functions of parent will be called automatically.
6.8 Run Time Type Information (RTTI)
You can get the type information of an object at the run time, as long as the type of the object has Run Time Type Information (RTTI) enabled. The RTTI describes the name and the inherience information of a class. By default, RTTI will not be generated for classes unless the classes has virtual functions. You can manually enable RTTI on a class by adding "@enable_rtti" before the "class" keyword of a class. The following example shows three classes with RTTI. Note that classes with virtual functions automatically include RTTI.
@enable_rtti
class A
end
class B
@virtual public function b()
end
end
class C
public c as int
@virtual public function b()
end
end
Note that if a class has RTTI enabled, all classes extending (inheriting from) it will be automatically marked RTTI-enabled. If a class is manually marked enable_rtti
, either it has no parent class, or it should extend a class with RTTI.
Given an expression, the RTTI data can be fetched by the keyword typeof
. The returned value of typeof(some_expression)
is an object of class type_info
, which contains the RTTI of the class of the expression. The class type_info
has a method to get the name of the class - get_name
, and it has a method
public function is_parent_of(child as type_info) as boolean
to check if another class (represented by RTTI) is inherited from the current class. Also, the type_info
class has a method
public function get_parent() as type_info
to get RTTI of the parent class. If a class has no parent class, the method returns null. See the following example:
dim a as B = new C
println(typeof(a).get_name()) # should print "XXXX.C"
println(typeof(a).get_parent().get_name()) # should print "XXXX.B"
The variable "a" is declared as an object in class B. But it is assigned with an instance of class C. Using typeof
operator, we can get the exact type of the variable "a".
The typeof
operator will execute the expression and extract the reference to the RTTI object at the run time. The expressions to be evaluated by typeof
should be of classes with RTTI, otherwise the compiler will throw an error.
A unique RTTI object will be created for each different class. Class template instances are different classes with different RTTIs.
Given a type, the RTTI can be fetched by a special function get_type_info[T]
defined in module rtti
. You can import this function by import rtti:get_type_info
. You need to replace T
with the class you need to fetch for RTTI. T
can only be classes with RTTI.
RTTI is useful when a variable is assigned with a subclass of the class which the variable is defined. Developers may want to check if the variable really holds an object in a subclass. Since RTTI for a class is unique, we can compare the references of type_info
(RTTI) by ===
to check that:
import rtti:get_type_info
dim a as B = new C
if typeof(a)===get_type_info[C]() then
println("the variable a is of class C")
end
The subclass checking and safe down-casting can be done with RTTI. Birdee provides the function dyn_cast
in the module rtti
to safely convert a superclass reference to a subclass reference.
import rtti:dyn_cast
dim a as B = new C
dim c as C = dyn_cast[C](a)
priintln(int2str(c.c))
The above code converts a variable "a" of superclass "B" to variable "c" of subclass "C", using dyn_cast[C]
. The function dyn_cast[C]
will convert the reference in the parameter to a reference of class "C". If the object pointered by the given parameter is not an instance of "C" or subclass of "C", dyn_cast[C]
will return null. You can replace 'C' here with other classes with RTTI. dyn_cast[...]
is a system provided function, which internally compares the RTTIs of the classes.
Enabling RTTI has some overhead in space. If a class has RTTI, all of its instance has one additional hidden member pointing to the type_info
object of the class.
6.9 Abstract Class & Interface
An abstrct class is a class that is declared abstract, through including abstract methods. Abstract method is a special kind of virtual method that have no implementation. Abstract classes cannot be instantiated, but they can be subclassed. Abstract methods can be declared as following:
class IamAbstractCls
public abstract func IamAbstractMethod()
end
When an abstract class is subclassed, the subclass usually provides implementations for all of the abstract methods in its parent class. However, if it does not, then the subclass must also be declared abstract.
An interface is a completely "abstract class" that is used to group related methods with empty bodies:
interface IamInterface
public abstract func a()
public abstract func b() as int
end
Interfaces can only be 'subinterfaced' by interfaces, and classes cannot subclass them. To access the interface methods, the interface must be "implemented" (kinda like inherited) by another class with the implements
keyword. The body of the interface method is provided by the "implement" class:
interface if
public abstract func a() as int
end
class implementer implements if
public func a() as int
return 0
end
end
If a class implementing one interface does not override all of its methods, then the class automatically becomes an abstract class.
Birdee does not support 'multiple inheritance', however, it can be achieved with interfaces, because the class can implement multiple interfaces:
interface foo
public abstract func a() as int
end
interface bar
public abstract func b() as int
end
class base
public A as int
end
class implementer : base implements foo, bar
public func a() as int
return 0
end
public func b() as int
return 1
end
end
;The type of interfaces is implemented with 'fat pointer', containing one pointer to itable and one pointer to objects, hence its memory layout is different from classes.
Programmers can cast object with class type to interface type with simple assignment =
, static_cast[]
or dyn_cast[]
:
interface foo
public abstract func a() as int
end
class implementer implements foo
public func a() as int
return 0
end
end
dim obj as implementer = new implementer
dim if as foo = obj
if = static_cast[foo](obj)
if = dyn_cast[foo](obj)
For interfaces, using assignment =
and static_cast[]
are similar, while it's always safer to use dyn_cast[]
.
Comparing Abstract Classes and Interfaces, when should we use them? Here are so references:
- Consider using abstract classes if any of these statements apply to your situation:
- You want to share code among several closely related classes.
- You expect that classes that inherit your abstract class have many common methods or fields.
- Consider using interfaces if any of these statements apply to your situation:
- You expect that unrelated classes would implement your interface.
- You want to specify the behavior of a particular data type, but not concerned about who implements its behavior.
- You want to take advantage of multiple inheritance of type.
6.10 Structs
Struct is a similar but different concept as class. Structs can be similarly defined as classes. You just need to replace "class" keyword with "struct".
struct {name}
...
end [struct]
The member variables and functions can be similarly defined and used in structs.
So what's the difference between struct and class? One key difference is that for local variables defined in functions, structs are allocated on the stack and class objects are allocated on the heap. The access and allocation of data on the stack is much faster than on the heap. Also, once the program leaves the scope of a function, the space of the local struct variables will be deallocated.
The second difference is that, in the context of Birdee, variables of "class" has "reference semantic", while variables of "struct" has "value semantic". A class variable (including local, global and member one) is always a reference to an object in the heap or null. If you copy a class object variable, you just copy the reference to the object, not the actual data of object. On the other hand, copying struct variables (by operator "=") or implicitly copying struct variables (in function parameters), you will copy the whole struct object. Hence, struct variables are "values", not "references".
If a class/struct, say "A", has a class, say "B", member variable, the class/struct A only holds a refernce to B. But if "B" is changed to struct, "B" will embeded into the memory layout of "A", which means allocating an object of "A" will implicitly allocate space for "B".
So you should be careful when the struct has many fields - copying these structs involves large amounts of memory copying.
Some notes on struct:
- Operator overloading is supported in structs.
- You cannot enable RTTI on struct
Important: The "__del__" methods of structs will not be automatically called when the structs objects are destroyed!